/*
 * Copyright 2012-2013 Finbudget.com
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package finbudgettable.client.ui.grid;

import com.google.gwt.cell.client.Cell;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.dom.client.TableCellElement;
import com.google.gwt.dom.client.TableRowElement;
import com.google.gwt.dom.client.TableSectionElement;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.user.cellview.client.*;
import com.google.gwt.user.client.DOM;
import com.google.gwt.view.client.ListDataProvider;
import com.google.gwt.view.client.ProvidesKey;
import com.gwtplatform.mvp.client.Presenter;

import finbudget.client.ui.table.Record;
import finbudgettable.client.ui.grid.cell.status.StatusColumn;
import finbudgettable.client.ui.grid.column.GridColumn;
import finbudgettable.client.ui.grid.dnd.DragAndDropController;
import finbudgettable.client.ui.grid.event.*;
import finbudgettable.client.ui.grid.style.CssController;

import java.util.*;

public class CellGrid extends NavigatableCellTable<Record> {

    public interface StandardResources extends CellTable.Resources {

        public interface Style extends CellTable.Style {
        }

        @Source("CellTable.css")
        Style cellTableStyle();
    }

    interface Resources extends ClientBundle {

        interface Style extends CssResource {
            @ClassName("b-cellgrid")
            String bCellgrid();
        }

        @Source("CellGrid.css")
        Style style();
    }

    private static class KeyProvider implements ProvidesKey<Record> {
        @Override
        public Object getKey(Record item) {
            return item == null ? null : item.getKey();
        }
    }

    public static final int PAGE_SIZE = 20;

    private final ListDataProvider<Record> dataProvider;
    public static final StandardResources STANDARD_RESOURCES = GWT.create(StandardResources.class);
    public static final Resources RESOURCES = GWT.create(Resources.class);

    static {
        RESOURCES.style().ensureInjected();
        STANDARD_RESOURCES.cellTableStyle().ensureInjected();
    }

    // the list of records that the grid contains
    private final List<Record> allRecords = new ArrayList<Record>();

    // the map of unchanged copies of records that were changed
    private final Map<String, Record> originalRecords = new HashMap<String, Record>();

    // the list of records that were changed
    private final List<Record> changedRecords = new ArrayList<Record>();

    private final List<Header<?>> headers = new ArrayList<Header<?>>();
    private final List<Header<?>> unmodifiedHeaders = Collections.unmodifiableList(headers);
    private final List<GridColumn<?>> columns = new ArrayList<GridColumn<?>>();
    private final List<GridColumn<?>> unmodifiedColumns = Collections.unmodifiableList(columns);
    private final List<Header<?>> footers = new ArrayList<Header<?>>();

    private final DragAndDropController dragAndDropController = new DragAndDropController();
    private final CssController cssController = new CssController();

    private final ColumnSortEvent.ListHandler<Record> sortHandler;
    private final ChangeHandler changeHandler;

    private boolean multilineStyle = false;

    private boolean canDeleteRecords;

    public CellGrid(ChangeHandler changeHandler) {
        super(PAGE_SIZE, STANDARD_RESOURCES, new KeyProvider());

        this.changeHandler = changeHandler;

        getElement().setId(DOM.createUniqueId());
        addStyleName(RESOURCES.style().bCellgrid());

        dataProvider = new ListDataProvider<Record>();
        dataProvider.addDataDisplay(this);

        sortHandler = new ColumnSortEvent.ListHandler<Record>(dataProvider.getList());
        addColumnSortHandler(sortHandler);

        setKeyboardPagingPolicy(KeyboardPagingPolicy.CHANGE_PAGE);
        addHandler(recordChangingEventHandler, RecordChangingEvent.TYPE);
    }

    public void setSortHandlerComparator(Column<Record, ?> column, Comparator<Record> comparator) {
        sortHandler.setComparator(column, comparator);
    }

    public void setData(List<Record> records) {
        clear();
        allRecords.addAll(records);
        if (changeHandler != null) {
            changeHandler.handleChanges(false);
        }
        filter();
    }

    /**
     * Returns all columns (visible and hidden)
     *
     * @return
     */
    public List<GridColumn<?>> getAllColumns() {
        return unmodifiedColumns;
    }

    /**
     * Returns all headers (visible and hidden)
     *
     * @return
     */
    public List<Header<?>> getAllHeaders() {
        return unmodifiedHeaders;
    }

    @Override
    public void insertColumn(int beforeIndex, Column<Record, ?> col, Header<?> header, Header<?> footer) {
        if (!(col instanceof GridColumn)) {
            throw new IllegalArgumentException("Column must be a GridColumn subclass");
        }
        if (getColumnByFieldName(((GridColumn<?>) col).getFieldName()) != null) {
            throw new IllegalArgumentException("The column with such field already exists");
        }
        boolean insertLast = beforeIndex == getColumnCount();
        super.insertColumn(beforeIndex, col, header, footer);
        int columnIndex;
        if (insertLast) {
            columnIndex = columns.size();
        } else {
            Column<Record, ?> nextColumn = getColumn(beforeIndex + 1);
            columnIndex = getInnerColumnIndex(nextColumn);
        }
        headers.add(columnIndex, header);
        columns.add(columnIndex, (GridColumn<?>) col);
        footers.add(columnIndex, footer);
        setColumnWidth(col, 100, Unit.PX);
    }

    /**
     * Removes only visible columns
     *
     * @param index visible column index
     */
    @Override
    public void removeColumn(int index) {
        Column<Record, ?> column = getColumn(index);
        super.removeColumn(index);
        removeInnerColumn(column);
    }

    private int getInnerColumnIndex(Column<Record, ?> col) {
        return columns.indexOf(col);
    }

    private void removeInnerColumn(Column<Record, ?> col) {
        int index = getInnerColumnIndex(col);
        if (index >= 0) {
            removeInnerColumn(index);
        }
    }

    private void removeInnerColumn(int index) {
        columns.remove(index);
        headers.remove(index);
        footers.remove(index);
    }

    /**
     * Removes visible and hidden columns
     *
     * @param col
     */
    @Override
    public void removeColumn(Column<Record, ?> col) {
        if (isColumnVisible(col)) {
            super.removeColumn(col);
        }
        removeInnerColumn(col);
    }

    public boolean hideColumn(Column<Record, ?> column) {
    	// Check if column is not the last that could be hidded
    	int count = 0;
    	getColumnCount();
    	for (GridColumn<?> c : columns) {
    		if (!(c instanceof StatusColumn)) {
    			if (getColumnIndex(c) >= 0) {
    				count++;
    			}
    		}
    	}
        if (count > 1) {
            int index = getColumnIndex(column);
            if (index >= 0) {
                super.removeColumn(index);
                return true;
            }
        }
        return false;
    }

    public Header<?> getHeader(Column<Record, ?> column) {
        return headers.get(getInnerColumnIndex(column));
    }


    public Header<?> getFooter(Column<Record, ?> column) {
        return footers.get(getInnerColumnIndex(column));
    }


    public boolean isColumnVisible(Column<Record, ?> column) {
        if (getInnerColumnIndex(column) == -1) {
            throw new IllegalArgumentException("The Column is not belong to the grid");
        }
        return getColumnIndex(column) >= 0;
    }

    public void showColumn(Column<Record, ?> column) {
        int index = getColumnIndex(column);
        if (index == -1) {
            int innerColumnIndex = getInnerColumnIndex(column);
            for (index = innerColumnIndex - 1; index >= 0; index--) {
                Column<Record, ?> currentColumn = columns.get(index);
                if (isColumnVisible(currentColumn)) {
                    int currentIndex = getColumnIndex(currentColumn);
                    super.insertColumn(currentIndex + 1, column, headers.get(innerColumnIndex), footers.get(innerColumnIndex));
                    return;
                }
            }
            super.insertColumn(0, column, headers.get(innerColumnIndex), footers.get(innerColumnIndex));
        }
    }

    @Override
    public void setColumnWidth(Column<Record, ?> column, String width) {
        super.setColumnWidth(column, width);
        if (column instanceof GridColumn) {
            ((GridColumn<?>) column).setWidth(width);
        }
    }

    @Override
    public String getColumnWidth(Column<Record, ?> column) {
        if (isColumnVisible(column)) {
            return super.getColumnWidth(column);
        } else {
            return ((GridColumn<?>) column).getWidth();
        }
    }

    public void sortColumn(Column<Record, ?> column) {
        ColumnSortList sortList = getColumnSortList();
        sortList.push(column);
        ColumnSortEvent.fire(this, sortList);
    }

    public void sortColumn(Column<Record, ?> column, boolean ascending) {
        ColumnSortList sortList = getColumnSortList();
        sortList.push(new ColumnSortList.ColumnSortInfo(column, ascending));
        ColumnSortEvent.fire(this, sortList);
    }

    private final GridFilter filter = new GridFilter();

    public GridFilter getFilter() {
        return filter;
    }

    public void filter() {
        List<Record> list = dataProvider.getList();
        list.clear();
        for (Record record : allRecords) {
            if (filter.match(record)) {
                list.add(record);
            }
        }
        ColumnSortEvent.fire(this, getColumnSortList());
        redraw();
    }

    /**
     * Moves the visible column at the specified position
     *
     * @param column
     * @param beforeIndex
     */
    public void moveColumn(Column<Record, ?> column, int beforeIndex) {
        String width = getColumnWidth(column);
        Header<?> header = getHeader(column);
        Header<?> footer = getFooter(column);
        boolean moveToLastPosition = getColumnCount() == beforeIndex;
        if (moveToLastPosition) {
            removeColumn(column);
            if (footer == null) {
            	insertColumn(getColumnCount(), column, header);
            } else {
            	insertColumn(getColumnCount(), column, header, footer);
            }
        } else {
            Column<Record, ?> beforeColumn = getColumn(beforeIndex);
            removeColumn(column);
            if (footer == null) {
            	insertColumn(getColumnIndex(beforeColumn), column, header);
            } else {
            	insertColumn(getColumnIndex(beforeColumn), column, header, footer);
            }
        }
        setColumnWidth(column, width);
    }

    private boolean readonly;

    public boolean isReadonly() {
        return readonly;
    }

    public void setReadonly(boolean readonly) {
        if (this.readonly != readonly) {
            this.readonly = readonly;
            fireEvent(new ReadOnlyChangedEvent(readonly));
        }
    }

    public List<Record> getChangedRecords() {
        return Collections.unmodifiableList(changedRecords);
    }

    public List<Record> getAllRecords() {
        return Collections.unmodifiableList(allRecords);
    }

    private final RecordChangingEventHandler recordChangingEventHandler = new RecordChangingEventHandler() {
        @Override
        public void onRecordChanging(RecordChangingEvent event) {
        	if (event != null && event.getRecord() != null) {
	            String key = event.getRecord().getKey();
	            if (!originalRecords.containsKey(key)) {
	            	// First change of the record
	                Record record = event.getRecord();
	                originalRecords.put(key, record.makeClone());
	                changedRecords.add(record);
	                if (changeHandler != null) {
	                    changeHandler.handleChanges(true);
	                }
	            }
	        }
        }
    };

    public void cancelChanges() {
        for (Record record : changedRecords) {
            int index = allRecords.indexOf(record);
            if (index >= 0) {
	            allRecords.remove(index);
	            allRecords.add(index, originalRecords.get(record.getKey()));
            }
        }
        originalRecords.clear();
        changedRecords.clear();
        invalidateCache();
        if (changeHandler != null) {
            changeHandler.handleChanges(false);
        }
        filter();
    }

    private void invalidateCache() {
        fireEvent(new CacheInvalidatedEvent());
    }

    public void clear() {
        invalidateCache();
        dataProvider.getList().clear();
        allRecords.clear();
        originalRecords.clear();
        changedRecords.clear();
        redraw();
    }

    public boolean isDataChanged() {
        return !changedRecords.isEmpty();
    }

    public void addRecord(Record record) {
    	if (record != null) {
    		if (record.key != null) {
    			if (originalRecords.get(record.key) != null) {
    				originalRecords.remove(record.key);
    				// Remove also from changed records
    				ArrayList<Record> toRemove = new ArrayList<Record>(1);
    				for (Record changed : changedRecords) {
    					if (changed.key != null && changed.key.equals(record.key)) {
    						toRemove.add(changed);
    					}
    				}
    				if (toRemove.size() != 0) {
    					changedRecords.removeAll(toRemove);
    				}
    			}
    		}
    		// Find record to be removed from allRecords
    		ArrayList<Record> toRemoveFromAll = new ArrayList<Record>(1);
    		for (Record existing : allRecords) {
    			if (existing.key != null && existing.key.equals(record.key)) {
    				toRemoveFromAll.add(existing);
    			}
    		}
    		allRecords.removeAll(toRemoveFromAll);
    		allRecords.add(record);
	        filter();
    	}
    }

    /**
     * Refresh all column's cells
     */
    public void refreshColumn(String fieldName) {
        if (!isAttached() || getAllColumns().isEmpty() || allRecords.isEmpty()) {
            return;
        }
        GridColumn<?> column = getColumnByFieldName(fieldName);
        if (!isColumnVisible(column)) {
            return;
        }
        int colIndex = getColumnIndex(column);
        List<Record> items = getVisibleItems();
        TableSectionElement table = getTableBodyElement();
        int rowIndex = 0;
        for (Record rec : items) {
            if (table.getRows().getLength() <= rowIndex) {
                break;
            }
            TableRowElement tr = table.getRows().getItem(rowIndex);
            TableCellElement td = tr.getCells().getItem(colIndex);
            Element parent = td.getFirstChildElement();
            SafeHtmlBuilder sb = new SafeHtmlBuilder();
            Cell.Context context = new Cell.Context(rowIndex + getPageStart(), colIndex, rec.getKey());
            column.render(context, rec, sb);
            parent.setInnerHTML(sb.toSafeHtml().asString());
            column.resetFocus(context, parent, rec);
            rowIndex++;
        }
    }

    /**
     * Refresh one column's cell for the specified record
     */
    public void refreshCell(Record record, String fieldName) {
        if (!isAttached() || getAllColumns().isEmpty() || allRecords.isEmpty()) {
            return;
        }
        GridColumn<?> column = getColumnByFieldName(fieldName);
        if (!isColumnVisible(column)) {
            return;
        }
        int colIndex = getColumnIndex(column);
        List<Record> items = getVisibleItems();
        TableSectionElement table = getTableBodyElement();
        int rowIndex = items.indexOf(record);
        if (table.getRows().getLength() <= rowIndex) {
            return;
        }
        TableRowElement tr = table.getRows().getItem(rowIndex);
        TableCellElement td = tr.getCells().getItem(colIndex);
        Element parent = td.getFirstChildElement();
        SafeHtmlBuilder sb = new SafeHtmlBuilder();
        Cell.Context context = new Cell.Context(rowIndex + getPageStart(), colIndex, record.getKey());
        column.render(context, record, sb);
        parent.setInnerHTML(sb.toSafeHtml().asString());
        column.resetFocus(context, parent, record);
    }

    public boolean isMultilineStyle() {
        return multilineStyle;
    }

    public void setMultilineStyle(boolean multilineStyle) {
        if (this.multilineStyle != multilineStyle) {
            this.multilineStyle = multilineStyle;
            fireEvent(new MultilineStyleChangedEvent(multilineStyle));
        }
        redraw();
    }

    public DragAndDropController getDragAndDropController() {
        return dragAndDropController;
    }

    public GridColumn<?> getColumnByFieldName(String fieldName) {
    	if (fieldName != null) {
            for (GridColumn<?> column : columns) {
                if (fieldName.equals(column.getFieldName())) {
                    return column;
                }
            }
    	}
        return null;
    }

    public CssController getCssController() {
        return cssController;
    }

    @Override
    protected void onLoad() {
        super.onLoad();
        redraw(); // to refresh items that can be initialized deferred
    }

    @Override
    public void redraw() {
        super.redraw();
        Scheduler.get().scheduleFinally(new Scheduler.ScheduledCommand() {
            @Override
            public void execute() {
                for (GridColumn<?> column : getAllColumns()) {
                    column.adjustWidth();
                }
            }
        });
    }

    public void showSavedResults(List<Record> list) {
    	if (list != null) {
    		// We've got here a list of changed items returned from the server.
    		// Some records were changed, some deleted but some records server
    		// failed to change or delete.
    		// Here we have to update two lists: original -- it should get
    		// updated items, and actually removed items should be removed from
    		// it. The second list (mapSource) should contain the original but
    		// failed records should be taken from the list returned from the server.
    		// To do this we'll step throw mapSource and define weather the item
    		// was changed, deleted or stable.
    		// Prepare map for the returned values
	        Map<String, Record> savedResult = new HashMap<String, Record>(list.size());
	        for (Record rec : list) {
	        	String originalKey = rec.getValueString(Record.fieldOriginalKey);
	        	if (originalKey != null) {
	        		// The record was first created on a client with surrogate key, then it was saved and obtained
	        		// new key. Original surrogate key is stored in fieldOriginalKey.
	        		savedResult.put(originalKey, rec);
	        		rec.setValue(Record.fieldOriginalKey, null);
	        	} else {
	        		savedResult.put(rec.getKey(), rec);
	        	}
	        }
	        List<Record> recordsToRemove = new ArrayList<Record>(changedRecords.size());
	        List<Record> recordsToAdd = new ArrayList<Record>(changedRecords.size());
	        for (Record record : changedRecords) {
	        	Record savedRec = savedResult.get(record.getKey());
	            if (record.toDelete) {
                    recordsToRemove.add(record);
	                if (savedRec != null) {
	                	// The record was not deleted on server. Replace it as it contains the message
	                    int index = allRecords.indexOf(record);
	                    if (index >= 0) {
		                    allRecords.remove(index);
		                    allRecords.add(index, savedRec);
	                    }
	                    recordsToAdd.add(savedRec);
	                } else {
	                	// The record was deleted on server.
	                	allRecords.remove(record);
	                    originalRecords.remove(record.getKey());
	                }
	            } else /*if (record.isChanged)*/ {
	                if (savedRec != null) {
	                	// Replace the record.
	                    recordsToRemove.add(record);
	                    int index = allRecords.indexOf(record);
	                    if (index >= 0) {
		                    allRecords.remove(index);
		                    allRecords.add(index, savedRec);
	                    }
	                    if (savedRec.isChanged) {
	                    	// The record is not saved. Leave it in the changed lists.
		                    recordsToAdd.add(savedRec);
	                    } else {
	                    	// The record was changed. Remove it from changed lists.
		                    originalRecords.remove(record.getKey());
	                    }
	                }
	            }
	        }
	        changedRecords.removeAll(recordsToRemove);
	        changedRecords.addAll(recordsToAdd);
	        filter();
    	}
        if (changeHandler != null) {
            changeHandler.handleChanges(isDataChanged());
        }
    }

    /**
     * Controls show or not delete column
     */
    public void setCanDeleteRecords(boolean canDeleteRecords, boolean hasTotals) {
    	this.canDeleteRecords = canDeleteRecords;
//        if (this.canDeleteRecords != canDeleteRecords) {
//            this.canDeleteRecords = canDeleteRecords;
//            if (canDeleteRecords) {
//                if (deleteColumn == null) {
//                    deleteColumn = DeleteColumn.builder(this, hasTotals).build();
//                } else {
//                    showColumn(deleteColumn);
//                }
//            } else {
//                hideColumn(deleteColumn);
//            }
//        }
    }

    public Presenter<?, ?> getMyPresenter() {
    	return changeHandler == null ? null : changeHandler.getMyPresenter();
    }

    public List<Record> getFilteredRecords() {
    	return dataProvider.getList();
    }

	public void createStatusColumn(boolean hasTotals) {
		// Check if status column is already exists
		for (int i = 0; i < getColumnCount(); i++) {
			if (getColumn(i) instanceof StatusColumn) {
				return;
			}
		}
		// Status column is not in the grid. Create it.
		new StatusColumn.Builder(this, hasTotals).build();
	}

	public boolean canDeleteRecords() {
		return canDeleteRecords;
	}

}
